import sys
from PySide6 import QtCore, QtWidgets, QtGui  
from typing import Optional, Tuple

class FloatingWindow(QtWidgets.QMainWindow):
    """拖出来的独立窗口"""
    closed = QtCore.Signal(QtWidgets.QWidget, str)  # 把页面和标题还回去

    def __init__(self, page: QtWidgets.QWidget, title: str):
        super().__init__(None)
        self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
        self.setWindowTitle(title)
        self.setCentralWidget(page)
        self.resize(400, 300)

    def closeEvent(self, a0: QtGui.QCloseEvent) -> None:
        # 关闭时把页面还给原主窗口
        self.closed.emit(self.centralWidget(), self.windowTitle())
        super().closeEvent(a0)


class DragTabWidget(QtWidgets.QTabWidget):
    """支持把标签拖出成独立窗口的 QTabWidget"""
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setAcceptDrops(True)
        self.setMovable(True)          # 允许在内部重排
        self._drag_start_pos: Optional[QtCore.QPoint] = None
        self._floating: set[FloatingWindow] = set()  # 记录已弹出的窗口
        QtWidgets.QApplication.setStartDragDistance(4)
        self.tabBar().installEventFilter(self)

    # ---------- 1. 开始拖动 ----------
    def mousePressEvent(self, e: QtGui.QMouseEvent) -> None:
        if e.button() == QtCore.Qt.LeftButton:
            self._drag_start_pos = e.pos()
        super().mousePressEvent(e)

    def mouseMoveEvent(self, e: QtGui.QMouseEvent) -> None:
        if not (e.buttons() & QtCore.Qt.LeftButton) or self._drag_start_pos is None:
            return
        if (e.pos() - self._drag_start_pos).manhattanLength() < 4:   # 阈值改小
            return

        index = self.tabBar().tabAt(self._drag_start_pos)
        if index == -1:
            return

        # 关键：如果鼠标还在 tabBar 矩形里，就当成“内部重排”，不启动外拖
        if self.tabBar().rect().contains(e.pos()):
            super().mouseMoveEvent(e)      # 让基类继续处理内部拖动
            return

        # 真正开始外拖
        mime = QtCore.QMimeData()
        mime.setData('application/x-tabindex', QtCore.QByteArray.number(index))
        drag = QtGui.QDrag(self)
        drag.setMimeData(mime)
        pixmap = self.tabBar().grab(self.tabBar().tabRect(index))
        drag.setPixmap(pixmap)
        drag.exec(QtCore.Qt.MoveAction)

    # ---------- 2. 拖出 / 拖回 ----------
    def dragEnterEvent(self, e: QtGui.QDragEnterEvent) -> None:
        if e.mimeData().hasFormat('application/x-tabindex'):
            e.acceptProposedAction()
            print("dragEnterEvent")

    def dropEvent(self, e: QtGui.QDropEvent) -> None:
        print("dropEvent")
        mime = e.mimeData()
        if not mime.hasFormat('application/x-tabindex'):
            print("not format application/x-tabindex")
            return
        index = int(mime.data('application/x-tabindex'))
        print("index=",index,",count=",self.count())
        if index < 0 or index >= self.count():
            return

        # 判断是“外部拖回”还是“内部重排”
        if e.source() is self:
            # 内部重排，QTabWidget 默认已处理
            e.accept()
            print("inner")
            return

        # 外部拖回：把 FloatingWindow 的页面插回
        page = self._take_floating_page(e.source())
        if page:
            self.addTab(page[0], page[1])
            e.accept()
            print("back")
        else:
            # 真正的“拖出”：把页面抽出来建新窗口
            self._detach_tab(index)
            print("out")
            e.accept()

    # ---------- 3. 抽离成独立窗口 ----------
    def _detach_tab(self, index: int):
        print("_detach_tab")
        title = self.tabText(index)
        icon  = self.tabIcon(index)
        page  = self.widget(index)
        if not page:
            return

        self.removeTab(index)
        floater = FloatingWindow(page, title)
        floater.setWindowIcon(icon)
        floater.closed.connect(self._return_floating)
        self._floating.add(floater)
        floater.show()

    # ---------- 4. 页面还回 ----------
    def _return_floating(self, page: QtWidgets.QWidget, title: str):
        self.addTab(page, title)
        sender = self.sender()
        if sender in self._floating:
            self._floating.remove(sender)

    # ---------- 5. 辅助：从 FloatingWindow 拿回页面 ----------
    def _take_floating_page(self, source) -> Optional[Tuple[QtWidgets.QWidget, str]]:
        for w in list(self._floating):
            if w is source:
                page  = w.centralWidget()
                title = w.windowTitle()
                w.removeCentralWidget()  # 防止 page 被删
                w.close()
                return page, title
        return None
    def eventFilter(self, obj, e):
        if obj is self.tabBar():
            if e.type() == QtCore.QEvent.MouseButtonPress:
                self._drag_start_pos = e.pos()
            elif e.type() == QtCore.QEvent.MouseMove:
                self._on_tabbar_mouse_move(e)
        return super().eventFilter(obj, e)
    def _on_tabbar_mouse_move(self, e: QtGui.QMouseEvent):
        if not (e.buttons() & QtCore.Qt.LeftButton) or self._drag_start_pos is None:
            return
        if (e.pos() - self._drag_start_pos).manhattanLength() < 4:
            return

        index = self.tabBar().tabAt(self._drag_start_pos)
        if index == -1:
            return

        # 还在 tabBar 矩形里 → 交给基类做内部重排
        if self.tabBar().rect().contains(e.pos()):
            return

        # 真正外拖
        mime = QtCore.QMimeData()
        mime.setData('application/x-tabindex', QtCore.QByteArray.number(index))
        drag = QtGui.QDrag(self)
        drag.setMimeData(mime)
        pixmap = self.tabBar().grab(self.tabBar().tabRect(index))
        drag.setPixmap(pixmap)
        drag.exec(QtCore.Qt.MoveAction)
        # 拖完后重置
        self._drag_start_pos = None


# -------------------- demo --------------------
if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    top = QtWidgets.QMainWindow()
    top.setWindowTitle("DragTabWidget Demo")
    drag_tab = DragTabWidget()
    for i in range(5):
        edit = QtWidgets.QTextEdit(f"Page {i}")
        drag_tab.addTab(edit, f"Tab {i}")
    top.setCentralWidget(drag_tab)
    top.resize(600, 400)
    top.show()
    sys.exit(app.exec())